package org.apache.ant.extensions.notify;

import java.nio.file.*;
import java.nio.file.WatchEvent.Kind;

import static java.nio.file.StandardWatchEventKinds.*;
import static java.nio.file.LinkOption.*;
import java.nio.file.attribute.*;
import java.io.*;
import java.util.*;

/**
 * Example to watch a directory (or tree) for changes to files.
 */

class WatchDir implements Runnable {

	private final WatchService watcher;
	private final Map<WatchKey, Path> keys;

	private FileEventsHandler handler;
	private long lastProcessed = System.currentTimeMillis();

	@SuppressWarnings("unchecked")
	static <T> WatchEvent<T> cast(WatchEvent<?> event) {
		return (WatchEvent<T>) event;
	}

	/**
	 * Register the given directory with the WatchService
	 */
	public void register(Path dir) throws IOException {
		WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE,
				ENTRY_MODIFY);
		keys.put(key, dir);
	}

	public void registerAll(final Path start) throws IOException {
		// register directory and sub-directories
		Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
			@Override
			public FileVisitResult preVisitDirectory(Path dir,
					BasicFileAttributes attrs) throws IOException {
				register(dir);
				return FileVisitResult.CONTINUE;
			}
		});
	}

	public void registerAll(String start) throws IOException {
		// register directory and sub-directories
		this.registerAll(Paths.get(start));
	}

	public WatchDir(FileEventsHandler handler) throws IOException {
		this.watcher = FileSystems.getDefault().newWatchService();
		this.keys = new HashMap<WatchKey, Path>();
		this.handler = handler;
	}

	public void run() {
		for (;;) {

			WatchKey key;
			try {
				key = watcher.take();
			} catch (InterruptedException x) {
				return;
			}

			Path dir = keys.get(key);
			if (dir == null) {
				System.err.println("WatchKey not recognized!!");
				continue;
			}

			// when file changes multiple events are triggers
			// resulting in a task being executed multiple times.
			// however we still need to loop to register created files

			for (WatchEvent<?> event : key.pollEvents()) {
				Kind<?> kind = event.kind();

				// Context for directory entry event is the file name of entry
				WatchEvent<Path> ev = cast(event);
				Path name = ev.context();
				Path child = dir.resolve(name);

				routeEvents(child, event);

				// if directory is created, and watching recursively, then
				// register it and its sub-directories
				if ((kind == ENTRY_CREATE)) {
					try {
						if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
							registerAll(child);
						}
					} catch (IOException x) {
						// ignore to keep sample readbale
					}
				}
			}

			// reset key and remove from set if directory no longer accessible
			boolean valid = key.reset();
			if (!valid) {
				keys.remove(key);
				// all directories are inaccessible
				if (keys.isEmpty()) {
					break;
				}
			}
		}
	}

	private boolean acceptedFile(Path child) {

		try {
			if (child.toString().endsWith("swp"))
				return false;
			else if (child.toString().endsWith("~"))
				return false;
			else if (!child.toFile().exists())
				return false;
			else if (child.toFile().isHidden())
				return false;
			else if (child.toFile().getCanonicalPath().startsWith("."))
				return false;
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}

	private boolean routeEvents(Path src, WatchEvent<?> event) {

		if (acceptedFile(src)) {
			
			// process once each 250 ms
			if ((System.currentTimeMillis() - lastProcessed) < 250)
				return false;

			lastProcessed = System.currentTimeMillis();
			handler.log("accepted File: " + src.toString());

			if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
				handler.fileCreated(src.toAbsolutePath().toString());
			} else if (event.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
				handler.fileDeleted(src.toAbsolutePath().toString());
			} else if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
				handler.fileModified(src.toAbsolutePath().toString());
			}
			return true;
		} else {
			return false;
		}
	}

}
